package com.medusa.aps.business.modules.basic.mp.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.medusa.aps.business.model.constant.RedisConstants;
import com.medusa.aps.business.model.enums.ProductionType;
import com.medusa.aps.business.model.enums.SourceEnum;
import com.medusa.aps.business.model.exception.BusinessErrorCode;
import com.medusa.aps.business.model.pagination.Page;
import com.medusa.aps.business.modules.basic.model.input.MaterialDTO;
import com.medusa.aps.business.modules.basic.model.input.MaterialListDTO;
import com.medusa.aps.business.modules.basic.model.input.MaterialParam;
import com.medusa.aps.business.modules.basic.model.output.MaterialVO;
import com.medusa.aps.business.modules.basic.mp.entity.Material;
import com.medusa.aps.business.modules.basic.mp.entity.MaterialClassification;
import com.medusa.aps.business.modules.basic.mp.entity.MaterialUnit;
import com.medusa.aps.business.modules.basic.mp.mapper.MaterialMapper;
import com.medusa.aps.business.modules.basic.mp.service.MaterialClassificationService;
import com.medusa.aps.business.modules.basic.mp.service.MaterialService;
import com.medusa.aps.business.modules.basic.mp.service.MaterialUnitService;
import com.medusa.aps.business.modules.demand.mp.entity.OrderInformation;
import com.medusa.aps.business.modules.demand.mp.service.OrderInformationService;
import com.medusa.aps.business.modules.modeling.mp.entity.Bom;
import com.medusa.aps.business.modules.modeling.mp.entity.BomItem;
import com.medusa.aps.business.modules.modeling.mp.entity.ProcessRoute;
import com.medusa.aps.business.modules.modeling.mp.service.BomItemService;
import com.medusa.aps.business.modules.modeling.mp.service.BomService;
import com.medusa.aps.business.modules.modeling.mp.service.ProcessRouteService;
import com.medusa.aps.business.modules.plan.mp.entity.SuggestedPlan;
import com.medusa.aps.business.modules.plan.mp.service.SuggestedPlanService;
import com.medusa.aps.business.util.ExcelUtils;
import com.medusa.aps.business.util.PercentageScheduleUtils;
import com.medusa.aps.business.common.model.resp.SystemCode;
import com.medusa.aps.business.common.redis.annotation.Redisson;
import com.medusa.aps.business.global.model.exception.GlobalException;
import com.medusa.aps.business.global.model.exception.collector.DistinctValidator;
import com.medusa.aps.business.global.model.exception.collector.ErrorCollector;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 物料服务实现类
 *
 * @author yuxh
 * @since 2023-08-14 17:37:47
 */
@Service("materialService")
public class MaterialServiceImpl extends ServiceImpl<MaterialMapper, Material> implements MaterialService {

    @Resource
    private BomService bomService;
    @Resource
    private ProcessRouteService processRouteService;
    @Resource
    private SuggestedPlanService suggestedPlanService;
    @Resource
    private MaterialClassificationService materialClassificationService;
    @Resource
    private MaterialMapper materialMapper;
    @Resource
    private BomItemService bomItemService;
    @Resource
    private OrderInformationService orderInformationService;
    @Resource
    private MaterialUnitService materialUnitService;

    /**
     * 分页查询所有数据
     *
     * @param materialParam 分页对象
     * @return 所有数据
     */
    @Override
    public Page<Material> selectAll(MaterialParam materialParam) {
        return materialMapper.selectAll(materialParam);
    }

    /**
     * 批量保存物料
     *
     * @param materials 物料集合
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    @Redisson(name = RedisConstants.MATERIAL_LOCK_KEY, waitTime = 1)
    @Redisson(name = RedisConstants.MATERIAL_CLASSIFICATION_LOCK_KEY, waitTime = 1)
    public void saveBatchMaterial(List<MaterialDTO> materials) {
        validatorMaterialCode(materials);
        List<Material> materialList = new ArrayList<>(materials.size());
        for (MaterialDTO materialDTO : materials) {
            materialDTO.setActualInventory(materialDTO.getInventory());
            materialDTO.setDataSource(SourceEnum.ARTIFICIAL);
            Material material = materialDTO.newMaterial();
            materialList.add(material);
        }
        this.saveBatch(materialList);
    }

    /**
     * 删除物料
     *
     * @param materialIds 物料id集合
     */
    @Override
    @Redisson(name = RedisConstants.MATERIAL_LOCK_KEY, waitTime = 1)
    public void deleteMaterials(Set<Long> materialIds) {
        Map<String, String> materialMap = this.lambdaQuery()
                .select(Material::getMaterialName, Material::getMaterialCode)
                .in(Material::getId, materialIds)
                .list().stream()
                .collect(Collectors.toMap(Material::getMaterialCode, Material::getMaterialName));

        if (MapUtil.isEmpty(materialMap)) {
            throw new GlobalException(SystemCode.DATA_NOT_EXIST.getMsg());
        }
        //校验BOM父项物料
        PercentageScheduleUtils.valid(
                bomService.lambdaQuery()
                        .select(Bom::getMaterialCode)
                        .in(Bom::getMaterialId, materialIds)
                        .list(),
                Bom::getMaterialCode,
                "物料编码:{}已建立BOM"
        );
        //校验BOM子项物料
        PercentageScheduleUtils.valid(
                bomItemService.lambdaQuery()
                        .select(BomItem::getMaterialCode)
                        .in(BomItem::getMaterialId, materialIds)
                        .list(),
                BomItem::getMaterialCode,
                "物料编码:{},已经建立BOM层级"
        );
        // 工艺流程
        PercentageScheduleUtils.valid(
                processRouteService.lambdaQuery()
                        .select(ProcessRoute::getMaterialCode)
                        .in(ProcessRoute::getMaterialId, materialIds)
                        .list(),
                ProcessRoute::getMaterialCode,
                "物料编码:{},已经建立工艺流程"
        );

        // 建议计划
        PercentageScheduleUtils.valid(
                suggestedPlanService.lambdaQuery()
                        .select(SuggestedPlan::getMaterialCode)
                        .in(SuggestedPlan::getMaterialId, materialIds)
                        .list(),
                SuggestedPlan::getMaterialCode,
                "物料编码:{},已生成建议计划"
        );

        //无需生产订单
        PercentageScheduleUtils.valid(
                orderInformationService.lambdaQuery()
                        .select(OrderInformation::getItemCode)
                        .in(OrderInformation::getMaterialId, materialIds)
                        .list(),
                OrderInformation::getItemCode,
                "物料编码:{},含有无需生产的订单"
        );
        boolean success = this.removeBatchByIds(materialIds);
        if (!success) {
            throw BusinessErrorCode.FAIL.exception();
        }
    }

    private void validatorMaterialCode(List<MaterialDTO> materials) {
        ErrorCollector errorCollector = DistinctValidator.valid(materials, MaterialDTO::getMaterialCode, "物料编码");
        if (errorCollector.hasError()) {
            throw new GlobalException(errorCollector.toString());
        }
        List<String> materialCodes = new ArrayList<>();
        for (MaterialDTO materialDTO : materials) {
            materialCodes.add(materialDTO.getMaterialCode());
            checkRequiredFields(materialDTO);
        }
        List<Material> materialDbs = this.lambdaQuery()
                .select(Material::getMaterialCode)
                .in(Material::getMaterialCode, materialCodes)
                .list();
        if (CollectionUtils.isEmpty(materialDbs)) {
            return;
        }
        if (materialDbs.size() == materialCodes.size()) {
            Map<Long, StringBuilder> errorMap = new TreeMap<>();
            for (int i = 0; i < materialDbs.size(); i++) {
                ExcelUtils.handlerErrorMessage(errorMap, i + 1L, StrUtil.format("物料编码【{}】系统已存在", materialDbs.get(i).getMaterialCode()));
            }
            ExcelUtils.validErrorMessageData(errorMap);
        }
        Set<String> materialCodeDbs = materialDbs.stream().map(Material::getMaterialCode).collect(Collectors.toSet());
        StringBuilder materialCodeErrors = new StringBuilder();
        for (int i = 0; i < materialCodes.size(); i++) {
            if (materialCodeDbs.contains(materialCodes.get(i))) {
                materialCodeErrors.append(StrUtil.format("第{}行：物料编码【{}】系统已存在;", i + 1, materialCodes.get(i)));
            }
        }
        throw new GlobalException(materialCodeErrors.toString());
    }

    private void checkRequiredFields(MaterialDTO materialDTO) {
        if (materialDTO.getProductionType() != ProductionType.SELF_MADE) {
            if (materialDTO.getFixedLeadTime() == null) {
                throw new GlobalException(StrUtil.format("物料编码:【{}】,外购和委外必填固定提前期", materialDTO.getMaterialCode()));
            }
            if (materialDTO.getFixedLeadTimeBatch() == null) {
                throw new GlobalException(StrUtil.format("物料编码:【{}】,外购和委外必填固定提前期批量", materialDTO.getMaterialCode()));
            }
            if (materialDTO.getPurchaseUnit() == null) {
                throw new GlobalException(StrUtil.format("物料编码:【{}】,外购和委外必填采购单位", materialDTO.getMaterialCode()));
            }
        }
    }

    /**
     * 根据参数查询物料列表
     *
     * @param materialList 参数
     * @return 物料列表
     */
    @Override
    public List<MaterialVO> getSupplierMaterials(MaterialListDTO materialList) {
        List<Material> materials = this.lambdaQuery()
                .select(Material::getId, Material::getMaterialCode, Material::getMaterialName, Material::getMaterialSpecification, Material::getProductionType)
                .like(StrUtil.isNotBlank(materialList.getMaterialCode()), Material::getMaterialCode, materialList.getMaterialCode())
                .like(StrUtil.isNotBlank(materialList.getMaterialName()), Material::getMaterialName, materialList.getMaterialName())
                .eq(materialList.getProductionType() != null, Material::getProductionType, materialList.getProductionType())
                .last("limit 500")
                .list();
        if (CollectionUtils.isEmpty(materials)) {
            return Collections.emptyList();
        }
        return materials.stream().map(material ->
                new MaterialVO()
                        .setId(material.getId())
                        .setMaterialCode(material.getMaterialCode())
                        .setMaterialName(material.getMaterialName())
        ).toList();
    }

    /**
     * 修改物料
     *
     * @param materialDTO 物料
     */
    @Override
    @Redisson(name = RedisConstants.MATERIAL_LOCK_KEY, waitTime = 1)
    @Redisson(name = RedisConstants.MATERIAL_CLASSIFICATION_LOCK_KEY, waitTime = 1)
    public void updateMaterial(MaterialDTO materialDTO) {
        Long materialId = materialDTO.getId();
        boolean exists = this.lambdaQuery()
                .ne(Material::getId, materialId)
                .eq(Material::getMaterialCode, materialDTO.getMaterialCode())
                .exists();
        if (exists) {
            throw new GlobalException(StrUtil.format("物料编码：【{}】系统已存在", materialDTO.getMaterialCode()));
        }
        checkRequiredFields(materialDTO);
        Material pre = this.getById(materialId);
        //查询分类名称
        Map<Integer, String> classNameMap = materialClassificationService.lambdaQuery()
                .select(MaterialClassification::getClassificationName, MaterialClassification::getId)
                .in(MaterialClassification::getId, CollUtil.set(false, pre.getMaterialClassificationId(), materialDTO.getMaterialClassificationId()))
                .list()
                .stream().collect(Collectors.toMap(MaterialClassification::getId, MaterialClassification::getClassificationName));
        pre.setClassificationName(classNameMap.getOrDefault(pre.getMaterialClassificationId(), null));
        if (!pre.getMaterialCode().equals(materialDTO.getMaterialCode())) {
            throw new GlobalException(StrUtil.format("物料编码:【{}】不允许修改", materialDTO.getMaterialCode()));
        }
        exists = materialUnitService.lambdaQuery()
                .eq(MaterialUnit::getId, materialDTO.getMaterialUnitId())
                .eq(MaterialUnit::getProductUnit, materialDTO.getProductUnit())
                .exists();
        if (!exists) {
            throw new GlobalException(StrUtil.format("生产单位【{}】，系统不存在", materialDTO.getProductUnit()));
        }
        Material after = materialDTO.updateMaterial(pre);
        after.setClassificationName(classNameMap.getOrDefault(after.getMaterialClassificationId(), null));
        this.lambdaUpdate()
                .eq(Material::getId, materialId)
                .set(Material::getMaterialName, after.getMaterialName())
                .set(Material::getMaterialClassificationId, after.getMaterialClassificationId())
                .set(Material::getProductionType, after.getProductionType())
                .set(Material::getMaterialSpecification, after.getMaterialSpecification())
                .set(Material::getManufacturingStrategy, after.getManufacturingStrategy())
                .set(Material::getInventory, after.getInventory())
                .set(Material::getActualInventory, after.getActualInventory())
                .set(Material::getSafetyInventory, after.getSafetyInventory())
                .set(Material::getMinimumOrderQuantity, after.getMinimumOrderQuantity())
                .set(Material::getMaximumOrderQuantity, after.getMaximumOrderQuantity())
                .set(Material::getFixedLeadTime, after.getFixedLeadTime())
                .set(Material::getFixedLeadTimeBatch, after.getFixedLeadTimeBatch())
                .set(Material::getFinishedProductRate, after.getFinishedProductRate())
                .set(Material::getMaterial, after.getMaterial())
                .set(Material::getMaterialUnitId, after.getMaterialUnitId())
                .set(Material::getProductUnit, after.getProductUnit())
                .set(Material::getPurchaseUnit, after.getPurchaseUnit())
                .update();
    }

}

